package com.adityachandel.booklore.util;

import com.adityachandel.booklore.model.MetadataClearFlags;
import com.adityachandel.booklore.model.dto.BookMetadata;
import com.adityachandel.booklore.model.entity.AuthorEntity;
import com.adityachandel.booklore.model.entity.BookMetadataEntity;
import com.adityachandel.booklore.model.entity.CategoryEntity;
import com.adityachandel.booklore.model.entity.MoodEntity;
import com.adityachandel.booklore.model.entity.TagEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static org.junit.jupiter.api.Assertions.*;

public class MetadataChangeDetectorTest {

    private BookMetadataEntity existingMeta;
    private BookMetadata newMeta;
    private MetadataClearFlags clearFlags;

    @BeforeEach
    void setup() {
        existingMeta = BookMetadataEntity.builder()
                .bookId(1L)
                .title("Original Title")
                .subtitle("Original Subtitle")
                .publisher("Original Publisher")
                .publishedDate(LocalDate.of(2020, 1, 1))
                .description("Original Description")
                .seriesName("Original Series")
                .seriesNumber(1.0f)
                .seriesTotal(5)
                .isbn13("9781234567890")
                .isbn10("1234567890")
                .asin("B012345678")
                .goodreadsId("12345678")
                .comicvineId("987654")
                .hardcoverId("hc123456")
                .googleId("google123")
                .pageCount(300)
                .language("en")
                .personalRating(4.5)
                .amazonRating(4.2)
                .amazonReviewCount(1500)
                .goodreadsRating(4.1)
                .goodreadsReviewCount(25000)
                .hardcoverRating(4.0)
                .hardcoverReviewCount(500)
                .titleLocked(false)
                .subtitleLocked(false)
                .publisherLocked(false)
                .publishedDateLocked(false)
                .descriptionLocked(false)
                .seriesNameLocked(false)
                .seriesNumberLocked(false)
                .seriesTotalLocked(false)
                .isbn13Locked(false)
                .isbn10Locked(false)
                .asinLocked(false)
                .goodreadsIdLocked(false)
                .comicvineIdLocked(false)
                .hardcoverIdLocked(false)
                .googleIdLocked(false)
                .pageCountLocked(false)
                .languageLocked(false)
                .personalRatingLocked(false)
                .amazonRatingLocked(false)
                .amazonReviewCountLocked(false)
                .goodreadsRatingLocked(false)
                .goodreadsReviewCountLocked(false)
                .hardcoverRatingLocked(false)
                .hardcoverReviewCountLocked(false)
                .coverLocked(false)
                .authorsLocked(false)
                .categoriesLocked(false)
                .moodsLocked(false)
                .tagsLocked(false)
                .authors(Set.of(
                        AuthorEntity.builder().id(1L).name("Author One").build(),
                        AuthorEntity.builder().id(2L).name("Author Two").build()
                ))
                .categories(Set.of(
                        CategoryEntity.builder().id(1L).name("Fiction").build(),
                        CategoryEntity.builder().id(2L).name("Mystery").build()
                ))
                .moods(Set.of(
                        MoodEntity.builder().id(1L).name("Dark").build(),
                        MoodEntity.builder().id(2L).name("Suspenseful").build()
                ))
                .tags(Set.of(
                        TagEntity.builder().id(1L).name("Thriller").build(),
                        TagEntity.builder().id(2L).name("Bestseller").build()
                ))
                .build();

        newMeta = BookMetadata.builder()
                .bookId(1L)
                .title("Original Title")
                .subtitle("Original Subtitle")
                .publisher("Original Publisher")
                .publishedDate(LocalDate.of(2020, 1, 1))
                .description("Original Description")
                .seriesName("Original Series")
                .seriesNumber(1.0f)
                .seriesTotal(5)
                .isbn13("9781234567890")
                .isbn10("1234567890")
                .asin("B012345678")
                .goodreadsId("12345678")
                .comicvineId("987654")
                .hardcoverId("hc123456")
                .googleId("google123")
                .pageCount(300)
                .language("en")
                .personalRating(4.5)
                .amazonRating(4.2)
                .amazonReviewCount(1500)
                .goodreadsRating(4.1)
                .goodreadsReviewCount(25000)
                .hardcoverRating(4.0)
                .hardcoverReviewCount(500)
                .titleLocked(false)
                .subtitleLocked(false)
                .publisherLocked(false)
                .publishedDateLocked(false)
                .descriptionLocked(false)
                .seriesNameLocked(false)
                .seriesNumberLocked(false)
                .seriesTotalLocked(false)
                .isbn13Locked(false)
                .isbn10Locked(false)
                .asinLocked(false)
                .goodreadsIdLocked(false)
                .comicvineIdLocked(false)
                .hardcoverIdLocked(false)
                .googleIdLocked(false)
                .pageCountLocked(false)
                .languageLocked(false)
                .personalRatingLocked(false)
                .amazonRatingLocked(false)
                .amazonReviewCountLocked(false)
                .goodreadsRatingLocked(false)
                .goodreadsReviewCountLocked(false)
                .hardcoverRatingLocked(false)
                .hardcoverReviewCountLocked(false)
                .coverLocked(false)
                .authorsLocked(false)
                .categoriesLocked(false)
                .moodsLocked(false)
                .tagsLocked(false)
                .authors(Set.of("Author One", "Author Two"))
                .categories(Set.of("Fiction", "Mystery"))
                .moods(Set.of("Dark", "Suspenseful"))
                .tags(Set.of("Thriller", "Bestseller"))
                .build();

        clearFlags = new MetadataClearFlags();
    }

    @Test
    void testIsDifferent_whenNoChanges_returnsFalse() {
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when no changes exist");
    }

    @ParameterizedTest(name = "{0} field change")
    @MethodSource("fieldChangeProvider")
    @DisplayName("isDifferent() detects changes in all metadata fields")
    void testIsDifferent_whenFieldChanges_returnsTrue(String fieldName, Consumer<BookMetadata> modifier) {
        modifier.accept(newMeta);
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should detect change in " + fieldName);
    }

    static Stream<Arguments> fieldChangeProvider() {
        return Stream.of(
            Arguments.of("title", (Consumer<BookMetadata>) m -> m.setTitle("New Title")),
            Arguments.of("subtitle", (Consumer<BookMetadata>) m -> m.setSubtitle("New Subtitle")),
            Arguments.of("publisher", (Consumer<BookMetadata>) m -> m.setPublisher("New Publisher")),
            Arguments.of("publishedDate", (Consumer<BookMetadata>) m -> m.setPublishedDate(LocalDate.of(2021, 6, 15))),
            Arguments.of("description", (Consumer<BookMetadata>) m -> m.setDescription("New Description")),
            Arguments.of("seriesName", (Consumer<BookMetadata>) m -> m.setSeriesName("New Series")),
            Arguments.of("seriesNumber", (Consumer<BookMetadata>) m -> m.setSeriesNumber(2.0f)),
            Arguments.of("seriesTotal", (Consumer<BookMetadata>) m -> m.setSeriesTotal(10)),
            Arguments.of("isbn13", (Consumer<BookMetadata>) m -> m.setIsbn13("9780987654321")),
            Arguments.of("isbn10", (Consumer<BookMetadata>) m -> m.setIsbn10("0987654321")),
            Arguments.of("asin", (Consumer<BookMetadata>) m -> m.setAsin("B098765432")),
            Arguments.of("goodreadsId", (Consumer<BookMetadata>) m -> m.setGoodreadsId("87654321")),
            Arguments.of("comicvineId", (Consumer<BookMetadata>) m -> m.setComicvineId("123456")),
            Arguments.of("hardcoverId", (Consumer<BookMetadata>) m -> m.setHardcoverId("hc654321")),
            Arguments.of("googleId", (Consumer<BookMetadata>) m -> m.setGoogleId("google456")),
            Arguments.of("pageCount", (Consumer<BookMetadata>) m -> m.setPageCount(350)),
            Arguments.of("language", (Consumer<BookMetadata>) m -> m.setLanguage("fr")),
            Arguments.of("personalRating", (Consumer<BookMetadata>) m -> m.setPersonalRating(4.8)),
            Arguments.of("amazonRating", (Consumer<BookMetadata>) m -> m.setAmazonRating(4.5)),
            Arguments.of("amazonReviewCount", (Consumer<BookMetadata>) m -> m.setAmazonReviewCount(2000)),
            Arguments.of("goodreadsRating", (Consumer<BookMetadata>) m -> m.setGoodreadsRating(4.3)),
            Arguments.of("goodreadsReviewCount", (Consumer<BookMetadata>) m -> m.setGoodreadsReviewCount(30000)),
            Arguments.of("hardcoverRating", (Consumer<BookMetadata>) m -> m.setHardcoverRating(4.2)),
            Arguments.of("hardcoverReviewCount", (Consumer<BookMetadata>) m -> m.setHardcoverReviewCount(750))
        );
    }

    @Test
    void testIsDifferent_whenNullClearFlags_returnsTrue() {
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, null);
        assertTrue(result, "Should return true when clear flags is null");
    }

    @Test
    void testIsDifferent_whenTitleLocked_changes_returnsTrue() {
        newMeta.setTitleLocked(true);
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when title lock changes");
    }

    @Test
    void testIsDifferent_whenCoverLocked_changes_returnsTrue() {
        newMeta.setCoverLocked(true);
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when cover lock changes");
    }

    @Test
    void testIsDifferent_whenFieldLocked_changes_returnsTrue() {
        existingMeta.setTitleLocked(true);
        newMeta.setTitleLocked(false);
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when field lock changes");
    }

    @Test
    void testIsDifferent_whenFieldLocked_noChange_returnsFalse() {
        existingMeta.setTitleLocked(true);
        newMeta.setTitleLocked(true);
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when field locks match");
    }

    @Test
    void testHasValueChanges_whenNoChanges_returnsFalse() {
        boolean result = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when no value changes exist");
    }

    @Test
    void testHasValueChanges_whenTitleChanged_returnsTrue() {
        newMeta.setTitle("New Title");
        boolean result = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when title value changes");
    }

    @Test
    void testHasValueChanges_whenLockedFieldChanged_returnsFalse() {
        existingMeta.setTitleLocked(true);
        newMeta.setTitle("New Title");
        boolean result = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when locked field changes");
    }

    @Test
    void testHasValueChanges_whenClearFlagSet_returnsTrue() {
        clearFlags.setTitle(true);
        boolean result = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when clear flag is set (even if values match)");
    }

    @Test
    void testHasValueChangesForFileWrite_whenNoChanges_returnsFalse() {
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when no value changes exist for file write");
    }

    @Test
    void testHasValueChangesForFileWrite_whenTitleChanged_returnsTrue() {
        newMeta.setTitle("New Title");
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when title value changes for file write");
    }

    @Test
    void testHasValueChangesForFileWrite_excludesRatingsForFileWrite() {
        // Change rating fields that should not be considered for file write
        newMeta.setAmazonRating(4.8);
        newMeta.setGoodreadsRating(4.4);
        newMeta.setHardcoverRating(4.3);
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when only rating fields change for file write");
    }

    @Test
    void testEdgeCase_nullToEmptyString_returnsFalse() {
        existingMeta.setTitle(null);
        newMeta.setTitle("");
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false for null to empty string transition");
    }

    @Test
    void testEdgeCase_nullToEmptyCollection_returnsFalse() {
        BookMetadataEntity testExisting = BookMetadataEntity.builder()
                .bookId(1L)
                .title("Original Title")
                .subtitle("Original Subtitle")
                .publisher("Original Publisher")
                .authors(null) // null authors, this is what we're testing
                .authorsLocked(false)
                .categories(Set.of(CategoryEntity.builder().id(1L).name("Fiction").build()))
                .categoriesLocked(false)
                .moods(Set.of(MoodEntity.builder().id(1L).name("Dark").build()))
                .moodsLocked(false)
                .tags(Set.of(TagEntity.builder().id(1L).name("Thriller").build()))
                .tagsLocked(false)
                .titleLocked(false)
                .subtitleLocked(false)
                .publisherLocked(false)
                .build();

        BookMetadata testNew = BookMetadata.builder()
                .bookId(1L)
                .title("Original Title")
                .subtitle("Original Subtitle")
                .publisher("Original Publisher")
                .authors(Set.of()) // empty set, this is what we're testing
                .authorsLocked(false)
                .categories(Set.of("Fiction"))
                .categoriesLocked(false)
                .moods(Set.of("Dark"))
                .moodsLocked(false)
                .tags(Set.of("Thriller"))
                .tagsLocked(false)
                .titleLocked(false)
                .subtitleLocked(false)
                .publisherLocked(false)
                .build();

        boolean result = MetadataChangeDetector.isDifferent(testNew, testExisting, clearFlags);
        assertFalse(result, "Should return false because toNameSet converts null to empty set, so null → empty collection is no change");
    }

    @Test
    void testEdgeCase_emptyStringToNull_returnsTrue() {
        existingMeta.setTitle("");
        newMeta.setTitle(null);
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true for empty string to null transition");
    }

    @Test
    void testEdgeCase_emptyCollectionToNull_returnsTrue() {
        BookMetadataEntity testExisting = BookMetadataEntity.builder()
                .bookId(1L)
                .title("Test Title")
                .authors(Set.of()) // empty set, what we're testing
                .authorsLocked(false)
                .categories(Set.of(CategoryEntity.builder().id(1L).name("Fiction").build()))
                .categoriesLocked(false)
                .moods(Set.of(MoodEntity.builder().id(1L).name("Dark").build()))
                .moodsLocked(false)
                .tags(Set.of(TagEntity.builder().id(1L).name("Tag").build()))
                .tagsLocked(false)
                .titleLocked(false)
                .build();

        BookMetadata testNew = BookMetadata.builder()
                .bookId(1L)
                .title("Test Title")
                .authors(null) // null, what we're testing
                .authorsLocked(false)
                .categories(Set.of("Fiction"))  // Match existing
                .categoriesLocked(false)
                .moods(Set.of("Dark"))          // Match existing
                .moodsLocked(false)
                .tags(Set.of("Tag"))            // Match existing
                .tagsLocked(false)
                .titleLocked(false)
                .build();
        boolean result = MetadataChangeDetector.isDifferent(testNew, testExisting, clearFlags);
        assertTrue(result, "Should return true for empty collection to null transition");
    }

    @Test
    @DisplayName("Locked field blocks value changes but still detects lock state changes")
    void testIsDifferent_whenFieldLockedAndValueDifferent_stillDetectsLockChange() {
        existingMeta.setTitleLocked(true);
        newMeta.setTitle("New Title"); // value change (should be ignored due to lock)
        newMeta.setTitleLocked(false); // lock change (should be detected)

        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should detect lock change even if value change is blocked by lock");

        boolean valueResult = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertFalse(valueResult, "Should not detect value change when field is locked");
    }

    @Test
    void testIsDifferent_whenFieldLockedAndOnlyValueDifferent_returnsFalse() {
        existingMeta.setTitleLocked(true);
        newMeta.setTitle("New Title");
        newMeta.setTitleLocked(true); // lock unchanged

        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should not detect value change when field is locked and lock is unchanged");
    }

    @Test
    void testClearFlag_whenFieldUnlocked_triggersChange() {
        BookMetadataEntity testExisting = BookMetadataEntity.builder()
                .bookId(1L)
                .title("Existing Title")
                .titleLocked(false) // field is unlocked
                .build();
        BookMetadata testNew = BookMetadata.builder()
                .bookId(1L)
                .title("Existing Title") // same title
                .titleLocked(false)
                .build();
        MetadataClearFlags testClearFlags = new MetadataClearFlags();
        testClearFlags.setTitle(true); // clear flag forces change detection

        boolean result = MetadataChangeDetector.hasValueChanges(testNew, testExisting, testClearFlags);
        assertTrue(result, "Clear flag should trigger change when field is unlocked, even with identical values");
    }

    @Test
    @DisplayName("Lock takes precedence over clear flag")
    void testHasValueChanges_whenClearFlagTrue_lockStillTakesPrecedence() {
        existingMeta.setTitleLocked(true);
        newMeta.setTitle("New Title");
        clearFlags.setTitle(true);

        boolean result = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Lock should take precedence over clear flag");
    }

    @Test
    void testClearFlag_whenExistingValueIsNull_returnsFalse() {
        existingMeta.setTitle(null);
        existingMeta.setTitleLocked(false);
        newMeta.setTitle("New Title");
        clearFlags.setTitle(true);

        boolean result = MetadataChangeDetector.hasValueChanges(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Clear flag with null existing value means nothing to clear");
    }

    @Test
    void testAuthorsSetComparison_isOrderInsensitive() {
        // Change order of authors in newMeta
        newMeta.setAuthors(Set.of("Author Two", "Author One"));
        // existingMeta has Set.of("Author One", "Author Two") from setup

        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Authors set comparison should be order insensitive");
    }

    @Test
    void testIsDifferent_whenClearFlagTrue_andValuesIdentical_returnsTrue() {
        // newMeta and existingMeta have identical title values
        clearFlags.setTitle(true); // but clear flag forces change detection

        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "isDifferent should return true when clear flag is set, even if values are identical");
    }

    @Test
    @DisplayName("hasValueChangesForFileWrite() excludes certain fields from triggering file writes")
    void testHasValueChangesForFileWrite_excludesPageCount() {
        newMeta.setPageCount(999);
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertFalse(result, "PageCount should not trigger file write");
    }

    @Test
    void testHasValueChangesForFileWrite_excludesMoods() {
        newMeta.setMoods(Set.of("New Mood"));
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Moods should not trigger file write");
    }

    @Test
    void testHasValueChangesForFileWrite_excludesTags() {
        newMeta.setTags(Set.of("New Tag"));
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Tags should not trigger file write");
    }

    @Test
    void testHasValueChangesForFileWrite_excludesRatingFields() {
        newMeta.setAmazonRating(4.9);
        newMeta.setGoodreadsRating(4.8);
        newMeta.setHardcoverRating(4.7);
        newMeta.setAmazonReviewCount(2000);
        newMeta.setGoodreadsReviewCount(35000);
        newMeta.setHardcoverReviewCount(800);
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Rating and review count fields should not trigger file write");
    }

    @Test
    void testHasValueChangesForFileWrite_includesAuthors() {
        newMeta.setAuthors(Set.of("New Author"));
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Authors change should trigger file write");
    }

    @Test
    void testHasValueChangesForFileWrite_includesCategories() {
        newMeta.setCategories(Set.of("New Category"));
        boolean result = MetadataChangeDetector.hasValueChangesForFileWrite(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Categories change should trigger file write");
    }

    @Test
    void testIsDifferent_whenBothSubtitleValuesNull_returnsFalse() {
        existingMeta.setSubtitle(null);
        newMeta.setSubtitle(null);

        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when both values are null");
    }

    @Test
    void testIsDifferent_whenMultipleFieldsChange_returnsTrue() {
        newMeta.setTitle("New Title");
        newMeta.setSubtitle("New Subtitle");
        newMeta.setPublisher("New Publisher");
        newMeta.setPageCount(999);

        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertTrue(result, "Should return true when multiple fields change simultaneously");
    }

    @Test
    void testEdgeCase_whitespaceNormalization() {
        existingMeta.setTitle("  Original Title  ");
        newMeta.setTitle("Original Title");
        boolean result = MetadataChangeDetector.isDifferent(newMeta, existingMeta, clearFlags);
        assertFalse(result, "Should return false when strings are equal after whitespace normalization");
    }
}
